package jastgen.generator;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Random;
import java.io.PrintWriter;

import jastgen.nodes.ClassDef;
import jastgen.nodes.Field;
import jastgen.nodes.Type;
import static jastgen.generator.Generator.*;

class ClassBodyGenerator implements BodyGenerator {

	private static final String
	strContext = "context",
	strHashCode = "hashCode",
	strRHS = "rhs";

	// インスタンス変数を持たないノードクラスのhashCodeは乱数 (クラスごとに1つ)
	private final Random rnd = new Random(System.currentTimeMillis());

	// 名前の衝突を防ぐために使用
	private final Map<String, String> implNameInMethod;

	private final List<String> visitReturnTypes;

	/* ノード型がインターフェースを持つ場合、
	 * ノード型同士のcompareToは実装されない。
	 * (インターフェース型同士で実装されるため不可能になる)
	 * 
	 * そのため、フィールドがノード型でインターフェースを持つ場合、
	 * compareToでコンパイルエラーが発生する。
	 * 
	 * これを防ぐため、
	 * ・getterで返す直前にキャストする。
	 * 　(ダウンキャストなので)
	 * ・フィールドの型をインターフェースの型にする。
	 * という変更が必要となる。
	 * コンストラクタでのフィールドへの代入は変更不要。
	 * (アップキャストなので)
	 * 
	 * 代入されるのはキャスト後の型のオブジェクトのみなので、
	 * キャストの型安全は100%保証される。
	 */
	private final ClassToInterface c2i;

	private final Map<String, List<String>> interfaceNameToMethodNames;

	private final ClassDef node;  // インスタンス変数にする必要は無いのでは?

	private final String
	uniqueContext,
	uniqueHashCode,
	uniqueRHS;

	ClassBodyGenerator(
			final List<String> visitReturnTypes,
			final ClassToInterface c2i,
			final Map<String, List<String>> interfaceNameToMethodNames,
			final ClassDef node)
	{
		this.visitReturnTypes = visitReturnTypes;
		this.c2i = c2i;
		this.interfaceNameToMethodNames = interfaceNameToMethodNames;
		this.node = node;
		uniqueContext = unique(strContext);
		uniqueHashCode = unique(strHashCode);
		uniqueRHS = unique(strRHS);
		implNameInMethod = implNameInMethod(node);
	}

	private Map<String, String> implNameInMethod(
			final ClassDef node)
	{
		final Map<String, String> m = new TreeMap<String, String>();
		for(final Field f : node.getFields()) {
			final String mn = f.getNameInMethod();
			m.put(mn, uniqueNameInMethodImpl(mn));
		}
		return m;
	}

	public void generate(
			final PrintWriter out)
	{
		generateClassBody(out, node);
	}

	private void generateClassBody(
			final PrintWriter out,
			final ClassDef node)
	{
		final String n = node.getName();

		out.print("public final class " + n);
		if(!node.hasInterface()) {

			final String ext = "implements Comparable<" + n + ">, Node";

			// クラス名が14文字を超えると1行に収まらない
			// (クラス名x2を除いた文字数は51, クラス名は2回出力)
			if(n.length() > 14) {
				out.println();
				out.print("	");
				out.println(ext);
				out.print("{");
			} else {
				out.println(" " + ext + " {");
			}

		} else {
			out.println(" extends " + node.getInterfaceName() + " {");
		}

		printFields(out, node);
		out.println();

		printFactory(out, node);
		out.println();

		printConstructor(out, node);
		out.println();

		for(final Field m : node.getFields()) {
			printGetField(out, m);
			out.println();
		}

		printVisit(out, node);
		out.println();

		printEquals(out, node);
		out.println();

		printEqualsForThisClass(out, node);
		out.println();

		printHashCode(out, node);
		out.println();

		printCompareTo(out, node);
		out.println();

		printToString(out);

		out.println("}");
	}

	private void printFields(
			final PrintWriter out,
			final ClassDef node)
	{
		out.println();
		if(isXXXX(node)) {
			out.println("	private static final " + node.getInterfaceOrClassBaseName() + " instance =");
			out.println("		new " + node.getBaseName() + "();");
		}
		for(final Field f : node.getFields()) {
			out.print("	private final " + c2i.getInterfaceOrClassName(f.getType()) + " " + f.getName() + ";");
			if(f.hasBody())
				out.print("  // cache");
			out.println();
		}
		if(node.hasNormalFields()) {
			out.println("	private final int " + uniqueHashCode + ";  // cache");
		}
	}

	private void printFactory(
			final PrintWriter out,
			final ClassDef node)
	{
		final String ifn = node.getInterfaceOrClassName();

		out.print("	public static ");
		out.print(node.getTypeParamsString());
		out.println(ifn + " get(");
		out.print("			final Context " + uniqueContext);
		{
			for(final Field m : node.getFields()) {
				if(m.hasBody())
					continue;
				out.println(",");
				out.print("			final " + m.getType() + " " + m.getName());
			}
		}
		out.println(")");
		out.println("	{");

		if(isXXXX(node)) {
			out.println("		return instance;");
		} else {
			out.println("		return " + uniqueContext + ".intern(");
			out.print("			new " + node.getBaseName() + node.getTypeParams() + "(");
			boolean is1st = true;
			if(node.hasFieldWithBody()) {
				out.println();
				out.print("					" + uniqueContext);
				is1st = false;
			}
			for(final Field f : node.getFields()) {
				if(f.hasBody())
					continue;
				if(!is1st) out.print(",");
				is1st = false;
				out.println();
				out.print("					" + f.getName());
			}
			out.println("));");
		}

		out.println("	}");
	}

	private void printConstructor(
			final PrintWriter out,
			final ClassDef node)
	{
		out.print("	private " + node.getBaseName() + "(");

		if(isXXXX(node)) {
			out.println(") {");
		} else {
			out.println();
			boolean is1st = true;
			if(node.hasFieldWithBody()) {
				out.print("			final Context " + uniqueContext);
				is1st = false;
			}
			for(final Field f : node.getFields()) {
				if(f.hasBody())
					continue;
				if(!is1st) out.println(",");
				is1st = false;
				out.print("			final " + f.getType() + " " + f.getName());
			}
			out.println(")");
			out.println("	{");
		}

		for(final Field f : node.getFields()) {
			out.print("		");
			if(node.hasNormalFields())
				out.print("this.");
			out.print(f.getName() + " = ");
			if(f.hasBody()) {
				final String getX = getOrIs(f) + implName(f.getNameInMethod()) + "(" + uniqueContext + ")";
				out.println(maybeUnmodifiable(f.getType(), getX) + ";  // cache");
			} else {
				out.println(maybeUnmodifiable(f.getType(), f.getName()) + ";");
			}
		}

		if(node.hasNormalFields())
			out.println("		this." + uniqueHashCode + " = hashCodeImpl();  // cache");

		out.println("	}");
	}

	private static String maybeUnmodifiable(
			final Type t,
			final String n)
	{
		if(t.canbeUnmodifiable())
			return "unmodifiable" + t.getName() + "(" + n + ")";
		return n;
	}

	private void printGetField(
			final PrintWriter out,
			final Field f)
	{
		final String mn = f.getNameInMethod();
		final Type t = f.getType();
		final String n = f.getName();

		// インターフェースで定義されたメソッドにはアノテーションを付ける
		if(node.hasInterface()) {
			final List<String> interfaceMethods =
				interfaceNameToMethodNames.get(node.getInterfaceBaseName());
			if(interfaceMethods.contains(mn))
				out.println("	@Override");
		}

		out.println("	public " + t + " " + getOrIs(f) + mn + "() {");
		out.print("		return ");
		if(c2i.hasInterface(t))
			out.print("(" + t + ")");
		out.println(n + ";");
		out.println("	}");

		// bodyで書いたメソッドの出力
		if(f.hasBody()) {
			out.println();
			out.println("	private " + t + " " + getOrIs(f) + implName(mn) + "(");
			out.println("			final Context context)");
			out.println("	" + f.getBody());
		}
	}

	// TODO: 正常に名前の衝突を防げるかテストする, unique_の改名
	// フィールドのbodyを実装するメソッドの名前のget以降
	private String implName(
			final String nameInMethod)
	{
		return implNameInMethod.get(nameInMethod);
	}

	private void printVisit(
			final PrintWriter out,
			final ClassDef node)
	{
		final String n = node.getBaseName();
		out.println("	@Override");
		out.println("	public void visit(final VoidVisitor v) {");
		out.println("		v.in" + n + "(this);");
		out.println("	}");
		out.println();
		if(node.hasInterface())
			out.println("	@Override");
		out.print("	public ");
		printTypeParams(out, visitReturnTypes, "\t        ");
		out.println();
		out.println("	" + toVisitReturnType(node.getInterfaceOrClassBaseName()) + " visit(");
		out.print("		final Visitor");
		printTypeParams(out, visitReturnTypes, "\t\t              ");
		out.print(" v)");
		out.println();
		out.println("	{");
		out.println("		return v.in" + n + "(this);");
		out.println("	}");
	}

	private void printEquals(
			final PrintWriter out,
			final ClassDef node)
	{
		final String n = node.getBaseName();
		out.println("	@Override");
		out.println("	public boolean equals(final Object rhs) {");
		out.println("		if(this == rhs)");
		out.println("			return true;");
		out.println("		if(rhs instanceof " + n + node.paramsToStringQ() + ")");
		out.println("			return equalsImpl((" + n + node.getTypeParams() + ")rhs);");
		out.println("		return false;");
		out.println("	}");
	}

	private void printEqualsForThisClass(
			final PrintWriter out,
			final ClassDef node)
	{
		out.println("	private boolean equalsImpl(");
		out.println("			final " + node.getBaseName() + node.getTypeParams() + " " + uniqueRHS + ")");
		out.print("	{");  // 直後の改行は以下で出力

		if(!node.hasNormalFields()) {
			out.println();
			out.println("		return true;");
		} else {
			boolean is1st = true;
			for(final Field f : node.getFields()) {
				if(f.isIgnored())
					continue;
				out.println();
				if(is1st) out.print("		return ");
				else      out.print("		    && ");
				is1st = false;
				final String n = f.getName();
				out.print("eq(" + n + ", " + uniqueRHS + "." + n + ")");
			}
			out.println(";");
		}

		out.println("	}");
	}

	private void printHashCode(
			final PrintWriter out,
			final ClassDef node)
	{
		out.println("	@Override");
		out.println("	public int hashCode() {");

		if(!node.hasNormalFields()) {
			out.println("		return " + rnd.nextInt() + ";");
		} else {
			out.println("		return " + uniqueHashCode + ";");
			out.println("	}");
			out.println();
			out.println("	private int hashCodeImpl() {");
			out.println("		int hash = 17;");
			boolean is1st = true;
			for(final Field f : node.getFields()) {
				if(f.isIgnored())
					continue;
				if(!is1st) out.println("		hash *= 31;");
				is1st = false;
				out.println("		hash += hash(" + f.getName() + ");");
			}
			out.println("		return hash;");
		}

		out.println("	}");
	}

	/* インターフェースが空の場合 => thisと同じクラスとの比較
	 * 空ではない場合 => そのインターフェース内での比較
	 */
	private void printCompareTo(
			final PrintWriter out,
			final ClassDef node)
	{
		final String cn = node.getName();
		final List<Field> ms = node.getFields();

		out.println("	@Override");

		if(!node.hasInterface()) {
			out.print("	public int compareTo");
		} else {
			final String ifn = node.getInterfaceName();
			out.println("	protected int compareToImpl(");
			out.println("			final " + ifn + " rhs)");
			out.println("	{");
			out.println("		return compareToImpl((" + cn + ")rhs);");
			out.println("	}");
			out.println();
			out.print("	private int compareToImpl");
		}

		out.println("(");
		out.println("			final " + cn + " " + uniqueRHS + ")");
		out.println("	{");

		if(node.hasNormalFields()) {
			if(!node.hasInterface()) {
				out.println("		if(this == " + uniqueRHS + ")");
				out.println("			return EQ;");
			}
			out.println("		int cmp;");
			for(final Field f : ms) {
				if(f.isIgnored())
					continue;
				final String n = f.getName();
				out.println("		cmp = cmp(" + n + ", " + uniqueRHS + "." + n + ");");
				out.println("		if(cmp != EQ) return cmp;");
			}
		}

		out.println("		return EQ;");
		out.println("	}");

		if(node.hasInterface()) {
			out.println();
			out.println("	@Override");
			out.println("	public int ordinal() {");
			out.println("		return " + node.getInterfaceNum() + ";");
			out.println("	}");
		}
	}

	private void printToString(
			final PrintWriter out)
	{
		out.println("	@Override");
		out.println("	public String toString() {");
		out.println("		return TreePrinter.toString(this);");
		out.println("	}");
	}

	// インスタンス変数名と重複しない名前を返す
	private String unique(
			final String id)
	{
		return Generator.unique(node, id);
	}

	// メソッド名と重複しない名前を返す
	private String uniqueNameInMethodImpl(
			final String id)
	{
		return Generator.uniqueNameInMethod(node, id+"Impl");
	}

	// TODO: 改名, ClassDefのメソッドにしては?
	private boolean isXXXX(final ClassDef node) {
		return !node.hasFieldWithBody() && !node.hasNormalFields();
	}
}
